Repository navigation
Conversation
… that is not set yet
The sorted walk listed the enumerable keys of a value through its
method table. For a module namespace that listing reads every export,
as Object.keys() does, and throws a ReferenceError for an export in its
temporal dead zone. Bun.inspect(ns, { sorted: true }), a Bun-format
snapshot and the diff of a failing toEqual() rethrew that error.
The walk now lists every key of a namespace, which reads no export, and
drops the non-enumerable keys by their slot. The read of each key
already skips one that throws, so the unset export is left out, as in
the plain print. Both namespace classes are covered: the one of the
module loader and the one of the ahead-of-time run time.
The ErrorEvent test in inspect.test.js takes the change of #47123, so
that it no longer depends on its line number.
|
Status
|
|
No actionable comments were generated in the recent review. WalkthroughOrdered property enumeration now detects module namespaces and skips exports whose reads throw. Tests cover sorted inspection and table, diff, and snapshot output for partially initialized namespaces. ChangesModule namespace inspection
Suggested reviewers: Merge Risk:
|
|
The What was measuredBun 1.4.4 (38afee6), the canary of
All four rows come from one statement. Since #44833 the print lists the global with Two ways to fix it1. A lister for a namespace sandbox, with no exception clear. This is what I wrote.
2. Catch and relist at the listing statement. A prototype from the review, below.
The decisionMust rows 2 to 4 print again, as on 1.4.4? Or is their error the intended result since #44833?
Material
Prototype of way 2, on
|
|
The follow-up for Jest's and Vitest's snapshot formats is #48264.
|
Problem
ReferenceError: Cannot access 'later' before initialization.when an export is not set yet:Bun.inspect(ns, { sorted: true }), a Bun-format snapshot, a failingtoEqual()diff. Bun 1.4.4 prints them.forEachPropertyOrderedImpl(src/jsc/bindings/bindings.cpp:6298) lists enumerable keys only. A namespace reads every export to answer that.Fix
test/js/bun/util/inspect.test.jsand five other files, plus one--aotprogram. All but one fail on main.Background
import * as nsgives. Aconst,letorclassexport throws when read before its declaration runs, as in an import cycle.Bun.inspectwithsorted, Bun-format snapshots and matcher diffs.bun test --aotnamespace class andexpect.objectContaining(ns).Downsides
vitest, and anode:vmcontext with such a sandbox.Notes
Repro, run as
bun a.mjs:Main throws. Bun 1.4.4 prints
Module {}. This PR printsModule { hoisted: [Function: hoisted] }, the same asBun.inspect(ns).A second way to get such a namespace, with no files:
new vm.SourceTextModule(source), thenlink(), then.namespace. Its exports stay unset untilevaluate()runs.Reach. Fuzzing found this. There is no user report. The commit that brought it in (9e76acc) is after
bun-v1.4.4, so no release has it.Mechanism.
JSModuleNamespaceObject::getOwnPropertyNamesdoes a[[GetOwnProperty]]on each export when it is asked for enumerable keys only. The specification requires that forObject.keys(ns), and this PR does not change it. The namespace class of the ahead-of-time run time does the same. Asked for every key, both return the names and read nothing. The unsorted walk already lists the keys that way. One exception: the namespace of a mocked module always lists the object of its mock with the enumerable-only mode.What other tools do (run here):
util.inspect(ns, { sorted: true })printslater: <uninitialized>.Object.keys(ns)throws.toMatchSnapshot(),toBe()andtoEqual()all fail with the ReferenceError.toBe()andtoEqual()messages print"later": undefined.This PR does not print
<uninitialized>. It keeps the sorted print equal to the plain print. For a linkednode:vmmodule, the export stays left out untilevaluate()runs.Considered: list every object with every key, as the unsorted walk does, with no test for a namespace. The drop by slot cannot work for every object. The
Getslot of a Proxy and of anode:vmglobal carries no enumerable flag, so their hidden keys would print and theirgettraps would run for them.Measurements, main b1305dd against this PR:
JSC::AOT::isModuleNamespaceadded by the walk (gdb hit counts): 0 for 100 plain objects, 0 for 100 compiled objects underbun test --aot, 1 for each--aotnamespace printed..textis 110,627,497 bytes in both builds. The debug frame grows 32 bytes (0x740to0x760).process.env,globalThis,Bun, anode:vmglobal): 24 rows differ. All 24 are sorted prints of a namespace with an unset export. They throw on main and print here.bun build --compile --aot: 0 of 3 sorted rows throw (main: 3 of 3).BUN_JSC_validateExceptionChecks=1on the new tests: 0 reports.isNamespace ||term, the hidden-key test ininspect.test.jsfails. Without!withNonEnumerable &&, the older-Bun snapshot insnapshot.test.tsfails. Without the--aotarm, the--aotprogram fails at the compiled run.One contrived print moves. A namespace that
require()gives can take a symbol key from script. With 16 or more keys, and an enumerable symbol whose description equals the name of an export, the sorted print can swap that export and that symbol against main. Example: 15 exportsk00tok14andSymbol("k05"). Main printsk05first. This PR prints the symbol first. The order of such a pair was arbitrary before: the sort is not stable and compares a symbol by its description. On a plain object the order changes with the number of keys too, in Bun 1.4.3 as well. No release prints the exports of a namespace in the sorted walk, so no released snapshot holds such a pair.Still throws after this PR, for a namespace with an unset export.
Part of the regression from 1.4.4, not fixed here:
vitest(src/jsc/bindings/SnapshotFormat.cpp:151). Bun 1.4.4 passed it. That format has its own printer, and Vitest prints"later": undefinedthere, so it needs its own decision on the output.node:vmcontext whose sandbox is the namespace or a Proxy of it, plain and sorted. Bun 1.4.4 printedModule {}. Since bun test: Vitest compatibility #44833 the global forwards the listing mode to its sandbox (src/jsc/bindings/NodeVM.cpp:1657). inspect: print a node:vm context global from its own property slots, sorted or not #47217 rewrites that listing and is the place for it. Its current head still throws.New with #44833, never printed:
Reflect.ownKeys()and the plain print too. Bun 1.4.4 fails at the import of the mock. bun test: describe a mocked module's export without calling its getter #47815 works in that area.Older than the regression, Bun 1.4.4 throws too:
Bun.inspect.table(ns)andBun.inspect.table([ns]): the rows and columns come from another key listing.expect(ns).toEqual({}): the comparison lists the enumerable keys. Jest throws too.Proxyof the namespace in a matcher diff: the Proxy lists its own keys.Bun.inspectprints a Proxy as its target, so that case is fixed.$$typeof, plain and sorted: the printer reads$$typeofbefore it walks the keys.A built-in matcher also loses its own message when code of the value throws while the value is printed (a custom inspect hook, a getter). That question is separate. This PR adds no exception clear, so it does not decide it.
Tests.
ErrorEventtest ininspect.test.jstakes the same change as test: make the ErrorEvent inspect snapshot independent of its line number #47123. The test moves past line 999 here, and its old snapshot depended on the width of the line numbers.snapshot.test.ts, wherebun test -ucannot rewrite it.--aotcase is a program undertest/aot/parallel/modules/.run-parallel.tsfails it on a release build of main and passes it here.console.logformatter #46729 (head 070a72c), which replaces the Rust formatter of Bun-format snapshots, all eight tests pass with no further change.Nearby open PRs that edit the same function or its callers: #47217, #46156, #47799, #37331, #38937, #47861, #46729, #47605, #47815, #47788, #47123. None removes this throw.
no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/util/inspect.test.js, test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts